module sw.upload.uploadmodelist;
/**
 * Uploaderのモードに関するモジュール
 */
version(Upload):
import nazo.http, std.uri, nazo.xhtml, nazo.date, std.string, nazo.file, nazo.savedata, std.regexp, nazo.hmac_md5, std.md5, std.path, std.conv, std.math, nazo.array, nazo.feed, nazo.sort;
import sw.mode, sw.contentmode, sw.cgiinfo, sw.siteconfig, sw.locale.locale, sw.unsupportedmode, sw.client, sw.menuitem,  sw.name, sw.upload.uploadlist, sw.modelist;
alias std.string.split split;
alias std.string.find find;

class UploadModeFactory:ModeFactory{
  mixin TModeFactory!();
  const static string prefix="upload";
  IMode getMode(string name){
    if(cfg.synchronizeURI.length){
      switch(name){
        case "write":
        case "delete":
          return new UnsupportedMode(cfg);
        default:
      }
    }
    switch(name){
      case "list":
      case "":
        return new ListMode(cfg);
      case "write":
        return new WriteMode(cfg);
      case "show":
        return new ShowMode(cfg);
      case "delete":
        return new DeleteMode(cfg);
      case "mimelist":
        return new MimeTypeListMode(cfg);
      case "rss":
      case "rss1.0":
        return new FeedMode!(FeedType.RSS,1.0)(cfg);
      case "rss2.0":
        return new FeedMode!(FeedType.RSS,2.0)(cfg);
      case "rss0.91":
        return new FeedMode!(FeedType.RSS,0.91)(cfg);
      case "atom":
        return new FeedMode!(FeedType.ATOM)(cfg);
      case "atom0.3":
        return new FeedMode!(FeedType.ATOM,0.3)(cfg);
      case "sitemap":
        return new FeedMode!(FeedType.SITEMAP)(cfg);
      default:
    }
    return new UnsupportedMode(cfg);
  }
}

string[] getKeys(UploadList list,string sortby="time"){
  string* t="ext" in Http.query;
  string[] ext=t?(*t).split(","):null;
  string[] l=sort(list,sortby);
  if(ext)
    l=fil!(string)(l,delegate bool(string a){
      foreach(string e;ext){
        if(a[$-e.length-1..$]=="."~e)return true;
      }
      return false;
    });
  t="fmax" in Http.query;
  uint fmax=t?.toUint(*t):uint.max;
  t="fmin" in Http.query;
  uint fmin=t?.toUint(*t):0;
  if(fmax!=uint.max||fmin!=0){
    l=fil!(string)(l,delegate bool(string a){
      uint filesize=.toUint(list.getList[a].split(":")[2]);
      if(filesize>=fmin&&filesize<=fmax)
        return true;
      return false;
    });
  }
  return l;
}

string[] sort(UploadList list,string by="time"){
  string[] keys=list.getList.keys;
  switch(by){
    case "time":
      keys=nazo.sort.sort(keys,delegate uint(string key){return .toUint(list.getList[key].split(":")[3]);});
      keys=keys.reverse;
      break;
    case "size":
      nazo.sort.sort(keys,delegate uint(string key){return .toUint(list.getList[key].split(":")[2]);});
      keys=keys.reverse;
      break;
    case "name":
      nazo.sort.sort(keys,delegate string(string key){return list.getList[key].split(":")[0];});
      break;
    case "filename":
      nazo.sort.sort(keys,delegate string(string key){return key;});
      break;
    default:
      assert(0);
  }
  return keys;
}

class ListMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override d_time lastModified(){
    return 0;
  }
  override int permission(){
    return Client.Authority.Guest | Client.Authority.Admin;
  }
  override string title(){
    return "アップローダ内ファイル一覧";
  }
  ///adminの時はtrip非表示
  override string contentBody(){
    string content;
    if(cfg.synchronizeURI!="")
      content="<p>このページは<a href=\""~cfg.synchronizeURI~"?mode=upload:list\">上流</a>のミラーです。</p>";
    else
      content=`<ul><li>他人の権利を侵害する、及び日本の法に違反するようなファイルのアップロードは禁止です。</li><li>`~std.string.toString(cfg.uploadMaxSize)~`KiB以下のファイルしかアップロードできません。</li><li>拡張子に関係なくアップロード可能です。ダウンロード時に付加されるMimeTypeについては<a href="`~Http.relPath~`?mode=upload:mimelist">付加するMimeType一覧</a>を見てください。</li></ul><form method="post" action="`~Http.relPath~`" enctype="multipart/form-data">`\n`<input type="hidden" name="mode" value="upload:write"/>`\n`<label>ファイル:<input name="file" type="file"/></label>`\n~(Client.authority==Client.Authority.Admin?"":`<label>名前(トリップ必須):<input type="text" name="name"/></label>`\n`<label>削除キー:<input type="password" name="pass"/></label>`\n)~`<input type="submit" value="上記に同意してアップロード"/>`\n`</form>`\n;
    UploadList list=new UploadList;
    string* t="n" in Http.query;
    uint n=t?.toUint(*t):0;
    t="c" in Http.query;
    uint itemPerPage=t?.toUint(*t):cfg.defaultItemPerPage;
    uint i=n*itemPerPage;
    t="sort" in Http.query;
    string sortby=t?*t:"time";
    string[] l=getKeys(list,sortby);
    uint upper=i+itemPerPage<l.length?i+itemPerPage:l.length;
    content~="<ul class=\"index\">";
    for(uint i2;i2<ceil(cast(float)l.length/itemPerPage);i2++)content~="<li><a href=\""~Http.relPath~"?mode=upload:list&amp;n="~std.string.toString(i2)~(itemPerPage==cfg.defaultItemPerPage?"":"&amp;c="~std.string.toString(itemPerPage))~"\">"~std.string.toString(i2)~"</a></li>";
    content~="</ul>";
    if(l.length<i)throw new Exception("This page number("~std.string.toString(n)~") is not found");
    content~="<table>\n<tr><th>ファイル名</th><th>名前</th><th>ファイルサイズ</th><th>日付</th><th>MD5</th>"~(cfg.synchronizeURI!=""?"":"<th>コマンド</th>")~"</tr>"\n;
    foreach(string name;l[i..upper]){
      string value=list.getList[name];
      string[] values=value.split(":");
      auto m=MimeTypes.getMimeTypeStruct(getExt(name));
      content~=`<tr><th><object datatype="image/png" data="`~Http.basePath~m.icon~`.png" width="16" height="16"></object><a href="`~Http.relPath~"?mode=upload:show&amp;name="~encode(name)~`" title="`~m.type~`">`~Escape.text(name)~"</a></th><td>"~Escape.text(values[0])~"</td><td>"~Escape.text(values[2])~"bytes</td><td>"~time2trim(values[3])~"</td><td>"~(values.length>4?values[4]:"")~"</td>"~(cfg.synchronizeURI!=""?"":`<td><form action="`~Http.relPath~`" method="post"><input name="mode" value="upload:delete" type="hidden"/><input name="name" value="`~Escape.arg(name)~`" type="hidden"/>`~(Client.authority==Client.Authority.Admin?"":`<label>削除キー:<input name="pass" type="text"/></label>`)~`<input type="submit" value="削除"/></form></td>`)~"</tr>"\n;
    }
    content~="</table>";
    return content;
  }
}

class WriteMode:XhtmlMode!(ModeMethod.POST){
  bool isSuccess=false;
  string faildReason;
  this(SiteConfig* cfg){
    super(cfg);
    /*uploadする処理*/
    string pass;
    string name;
    string* t;
    if(Client.authority!=Client.Authority.Admin){
      t="pass" in Http.query;
      if(!t||*t==""){
        faildReason="削除キーが存在しません。";
        return;
      }
      pass=*t;
      t="name" in Http.query;
      if(!t||*t==""){
        faildReason="名前が存在しません。";
        return;
      }
      if((*t).find("#")==-1){
        faildReason="トリップが存在しません。";
        return;
      }
      name=*t;
    }
    auto t2="file" in Http.files;
    if(!t2||(*t2).name==""){
      faildReason="ファイルが存在しません。";
      return;
    }
    File* file=*t2;
    if(file.data.length>cfg.uploadMaxSize*1024){
      faildReason="ファイルサイズが大きすぎます("~std.string.toString(cfg.uploadMaxSize)~"KiB以下でないとだめ。)。";
      return;
    }
    name=getNameWithTrip(cfg,name);
    if(!exists("upload"))
      mkdir("upload");
    UploadList list=new UploadList;
    if(list.getValue(file.name)!=""){//削除キーが一緒なら上書きできるようにしたい
      faildReason="既にファイルが存在します。";
      return;
    }
    string md5;
    ubyte[16] _md5;
    pass.hmacMd5(cfg.passhash, _md5);
    md5 = std.base64.encode(cast(string)_md5);
    ubyte[16] file_md5;
    sum(file_md5,file.data);
    list.setValue(file.name,name~":"~md5~":"~std.string.toString(file.data.length)~":"~std.string.toString(.toUtime(getUTCtime()))~":"~tolower(digestToString(file_md5)));
    list.flash;
    write("upload/"~encode(file.name).replace("/","%2F").replace(".","%2E")~".tmp",file.data);
    if(cfg.uploadMaxItem!=0&&cfg.uploadMaxItem<list.getList.length){
      string del_item=sort(list)[0];
      list.removeValue(del_item);
      list.flash();
      remove("upload/"~encode(del_item).replace("/","%2F").replace(".","%2E")~".tmp");
    }
    isSuccess=true;
  }
  override string title(){
    return isSuccess?"アップロード完了":"アップロード失敗";
  }
  override string contentBody(){
    return isSuccess?`<p>`~Escape.text(Http.files["file"].name)~`のアップロードが完了しました。</p><ul><li><a href="`~Http.relPath~`?mode=upload:list">一覧</a></li></ul>`:"アップロードに失敗しました。"~faildReason~"一度前のページに戻って再度試してください。";
  }
}


class DeleteMode:XhtmlMode!(ModeMethod.POST){
  bool isSuccess=false;
  string faildReason;
  string name;
  this(SiteConfig* cfg){
    super(cfg);
    name=decodeComponent(Http.query["name"].replace("+"," "));
    UploadList list=new UploadList;
    string val=list.getValue(name);
    if(val==""){
      faildReason="ファイルが存在しません。";
      return;
    }
    if(Client.authority!=Client.Authority.Admin){
      auto t="pass" in Http.query;
      if(!t||*t==""){
        faildReason="削除キーが存在しません。";
        return;
      }
      string pass=*t;
      string del_md5=val.split(":")[1];
      if(del_md5==""){
        faildReason="あなたはそのファイルを消せません。";
        return;
      }
      string md5;
      ubyte[16] _md5;
      pass.hmacMd5(cfg.passhash, _md5);
      md5 = std.base64.encode(cast(string)_md5);
      if(del_md5!=md5){
        faildReason="パスワードが違います。";
        return;
      }
    }
    list.removeValue(name);
    list.flash();
    remove("upload/"~encode(name).replace("/","%2F").replace(".","%2E")~".tmp");
    isSuccess=true;
  }
  override string title(){
    return isSuccess?"削除完了":"削除失敗";
  }
  override string contentBody(){
    return isSuccess?`<p>`~Escape.text(name)~`の削除が完了しました。</p><ul><li><a href="`~Http.relPath~`?mode=upload:list">一覧</a></li></ul>`:"削除に失敗しました。"~faildReason~"一度前のページに戻って再度試してください。";
  }
}

invariant struct MimeType{
  string name;
  string type;
  string icon;
}

static struct MimeTypes{
  static invariant string[] types=["j2c","jp2","jpg","jpeg","jpe","gif","png","wdp","bmp","tiff","tif","svg","tlg","eri",
                                   "mid","mp2","mp3","mpga","ogg","wav","tta","wma","aa3","m4a","m4b","m4p","3g2","aac",
                                   "asf","asx","avi","wmv","swf","flv","mng","mpe","mpeg","mpg",
                                   "zip","lzh","lha","rar","7z","cab","tar","tgz","gz","bz2",
                                   "jar","xpi","deb","rpm",
                                   "css","dtd","txt","c","cpp","cxx","cc","h","d","di","ddoc","js","vbs","sh","diff","patch"];
  static invariant(MimeType) getMimeTypeStruct(string type){
    //決してxmlやhtmlやxhtmlを追加してはならない(クッキー漏れ対策。IEには効かない)。
    switch(type){
      //image
      case "j2c":
      case "jp2":
        enum invariant(MimeType) jpeg2000={"Jpeg2000","image/jp2","image"};
        return jpeg2000;
      case "jpg":
      case "jpeg":
      case "jpe":
        enum invariant(MimeType) jpeg={"Jpeg","image/jpeg","image"};
        return jpeg;
      case "gif":
        enum invariant(MimeType) gif={"Gif","image/gif","image"};
        return gif;
      case "png":
        enum invariant(MimeType) png={"Png","image/png","image"};
        return png;
      case "wdp":
        enum invariant(MimeType) wdp={"WMPhoto","image/vnd.ms-photo","image"};
        return wdp;
      case "bmp":
        enum invariant(MimeType) bmp={"Bitmap","image/bmp","image"};
        return bmp;
      case "tiff":
      case "tif":
        enum invariant(MimeType) tiff={"Tiff","image/tiff","image"};
        return tiff;
//      case "svg"://安全だと信じていいのだろうか? -> XSSがあるなぁ。
//        enum invariant(MimeType) svg={"SVG","image/svg+xml","image"};
//        return svg;
      case "tlg":
        enum invariant(MimeType) tlg={"TLG5/TLG6","application/octet-stream","image"};
        return tlg;
      case "eri":
        enum invariant(MimeType) eri={"ERI","application/octet-stream","image"};
        return eri;
      //audio
      case "mid":
        enum invariant(MimeType) midi={"Midi","audio/x-midi","audio"};
        return midi;
      case "mp2":
      case "mp3":
      case "mpga":
        enum invariant(MimeType) mpeg={"Mpeg","audio/mpeg","audio"};
        return mpeg;
      case "ogg":
        enum invariant(MimeType) ogg={"Ogg","application/ogg","audio"};
        return ogg;
      case "wav":
        enum invariant(MimeType) wav={"Wav","audio/x-wav","audio"};
        return wav;
      case "tta":
        enum invariant(MimeType) tta={"True Type Audio","application/octet-stream","audio"};
        return tta;
      case "wma":
        enum invariant(MimeType) wma={"Windows Media Audio","audio/x-wma","audio"};
        return wma;
      case "aa3":
        enum invariant(MimeType) aa3={"ATRAC3","application/octet-stream","audio"};
        return aa3;
      case "m4a":
      case "m4b":
      case "m4p":
      case "3g2":
      case "aac":
        enum invariant(MimeType) aac={"AAC","application/octet-stream","audio"};
        return aac;
      //video
      case "asf":
        enum invariant(MimeType) asf={"Advanced Systems Format","video/x-ms-asf","video"};
        return asf;
      case "asx":
        enum invariant(MimeType) asx={"Advanced Stream Redirector","video/x-ms-asf","video"};
        return asx;
      case "avi":
        enum invariant(MimeType) avi={"Audio Video Interleave","video/x-msvideo","video"};
        return avi;
      case "wmv":
        enum invariant(MimeType) wmv={"Windows Media Player","video/x-ms-wmv","video"};
        return wmv;
      case "swf":
        enum invariant(MimeType) swf={"Shockwave Flash","application/x-shockwave-flash","video"};
        return swf;
      case "flv":
        enum invariant(MimeType) flv={"Flash Video","video/x-flv","video"};
        return flv;
      case "mng":
        enum invariant(MimeType) mng={"Mng","video/x-mng","video"};
        return mng;
      case "mpe":
      case "mpeg":
      case "mpg":
        enum invariant(MimeType) mpeg_video={"Mpeg","video/mpeg","video"};
        return mpeg_video;
      //archive
      case "zip":
        enum invariant(MimeType) zip={"ZIP","application/zip","archive"};
        return zip;
      case "lzh":
      case "lha":
        enum invariant(MimeType) lzh={"LZH","application/x-lzh-compressed","archive"};
        return lzh;
      case "rar":
        enum invariant(MimeType) rar={"RAR","application/x-rar-compressed","archive"};
        return rar;
      case "7z":
        enum invariant(MimeType) _7z={"7Z","application/x-7z-compressed","archive"};
        return _7z;
      case "cab":
        enum invariant(MimeType) cab={"Cabinet","application/vnd.ms-cab-compressed","archive"};
        return cab;
      case "tar":
        enum invariant(MimeType) tar={"Tar","application/x-tar","archive"};
        return tar;
      case "tgz":
        enum invariant(MimeType) tgz={"Compressed Tar","application/x-compressed-tar","archive"};
        return tgz;
      case "gz":
        enum invariant(MimeType) gz={"Gzip","application/x-gzip","archive"};
        return gz;
      case "bz2":
        enum invariant(MimeType) bz2={"Bzip2","application/x-bzip","archive"};
        return bz2;
      //package
      case "jar":
        enum invariant(MimeType) jar={"Java Archive","application/octet-stream","archive"};
        return jar;
      case "xpi":
        enum invariant(MimeType) xpi={"XPInstall","application/x-xpinstall","archive"};
        return xpi;
      case "deb":
        enum invariant(MimeType) deb={"Debian package","application/x-deb","archive"};
        return deb;
      case "rpm":
        enum invariant(MimeType) rpm={"RPM Package","application/x-rpm","archive"};
        return rpm;
      //text
      case "css":
        enum invariant(MimeType) css={"Cascading Style Sheets","text/css","text"};
        return css;
      case "dtd":
        enum invariant(MimeType) dtd={"Document Type Definition","application/xml-dtd","text"};
        return dtd;
      case "txt":
        enum invariant(MimeType) txt={"Plain Text","text/plain","text"};
        return txt;
      case "c":
        enum invariant(MimeType) c={"C Source File","text/plain","text"};
        return c;
      case "cpp":
      case "cxx":
      case "cc":
        enum invariant(MimeType) cpp={"C++ Source File","text/plain","text"};
        return cpp;
      case "h":
        enum invariant(MimeType) h={"C, C++, Objective-C or Objective-C++ Header File","text/plain","text"};
        return h;
      case "d":
        enum invariant(MimeType) d={"D Source File","text/plain","text"};
        return d;
      case "di":
        enum invariant(MimeType) di={"D Header File","text/plain","text"};
        return di;
      case "ddoc":
        enum invariant(MimeType) ddoc={"Ddoc File","text/plain","text"};
        return ddoc;
      case "js":
        enum invariant(MimeType) js={"Javascript","application/javascript","text"};
        return js;
      case "vbs":
        enum invariant(MimeType) vbs={"VisualBasic Script","text/plain","text"};
        return vbs;
      case "sh":
        enum invariant(MimeType) sh={"Shell Script","text/plain","text"};
        return sh;
      case "diff":
      case "patch":
        enum invariant(MimeType) diff={"Diff","text/plain","text"};
        return diff;
      default:
    }
    enum invariant(MimeType) unknown={null,"application/octet-stream","unknown"};
    return unknown;
  }
  static enum string listHTML = getList();
  private static string getList(){
    string content="<table><tr><th>アイコン</th><th>拡張子</th><th>仕様</th><th>MimeType</th></tr>";
    foreach(string ext;types){
      auto data=getMimeTypeStruct(ext);
//      assert(data.name); // why failed?
      content~=`<tr><td><object datatype="image/png" data="`~data.icon~`.png" width="16" height="16"></object></td><td>`~ext~"</td><td>"~data.name~"</td><td>"~data.type~"</td></tr>";
    }
    content~="</table>";
    return content;
  }
}

class MimeTypeListMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override d_time lastModified(){
    return 0;
  }
  override string title(){
    return "付加するMimeType一覧";
  }
  override string contentBody(){
    return MimeTypes.listHTML;
  }
}

class ShowMode:Mode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Guest | Client.Authority.Admin;
  }
  override void response(){
    string name=decodeComponent(Http.query["name"].replace("+"," "));
    string path="upload/"~encode(name).replace("/","%2F").replace(".","%2E")~".tmp";
    ubyte[] data=cast(ubyte[])read(path);
    string[string] headers;
    string mimetype=MimeTypes.getMimeTypeStruct(getExt(name)).type;
    if(mimetype!="application/octet-stream"){
      headers["Content-Type"]=mimetype;
      headers["Content-Disposition"]=`inline; filename="`~encodeComponent(name)~`"`;
    }else{
      headers["Content-Type"]="application/octet-stream";
      headers["Content-Disposition"]=`attachment; filename="`~encodeComponent(name)~`"`;
    }
    headers["Last-Modified"] = toUTCString(getLastWriteTime(path));
    HttpResponse.write(data,200,headers);
  }
}

class FeedMode(FeedType type,double ver=0.0):XmlMode!(){
  UploadList ul;
  this(SiteConfig* cfg){
    super(cfg);
    ul=new UploadList;
  }
  override string filename(){
    return "upload-feed.xml";
  }
  override d_time lastModified(){
    return ul.lastModified;
  }
  ///RSSのアイテムの最大個数
  const uint maxlength=15;
  override string content(){
    string uri=Http.path();
    scope feed=new Feed!(type, ver)(Http.path~"?mode=upload:list","アップローダ内ファイル一覧 - "~cfg.siteName,"",cfg.defaultLanguage);
    uint i=0;
    string[] l=getKeys(ul);
    foreach(string name;l){
      string li=ul.getList[name];
      if(i++>this.maxlength)break;
      if(li!=""){//本来は必要無いはず…だけど不安なので入れとく
        string[] s=.split(li,":");
        string itemuri=Http.path~"?mode=upload:show&amp;name="~encode(name);
        feed.addItem(new FeedItem(itemuri,name,getUTCtimeFromUtime(.toUint(s[3]))));
      }
    }
    return feed.getContent();
  }
}
